/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.ws.rebind.util;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.ConfigurationProperty;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.SelectionProperty;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;

import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

/**
 * Copied from GWT internal code
 */
public class Shared
{
	/**
	 * Property used to control whether or not the RPC system will emit warnings
	 * when a type has final fields.
	 */
	public static final String WS_PROP_SUPPRESS_NON_STATIC_FINAL_FIELD_WARNINGS = "gwt.suppressNonStaticFinalFieldWarnings";

	/**
	 * Multi-valued configuration property used to list classes that are
	 * (potentially) enhanced with server-only fields, to be handled specially
	 * by WS.
	 */
	public static final String WS_ENHANCED_CLASSES = "ws.enhancedClasses";

	/**
	 * Capitalizes a name.
	 * 
	 * @param name the string to be capitalized
	 * @return the capitalized string
	 */
	static String capitalize(String name)
	{
		return name.substring(0, 1).toUpperCase(Locale.US) + name.substring(1);
	}

	/**
	 * Returns a Set of names of classes that may be enhanced with extra
	 * server-only fields.
	 * 
	 * @param propertyOracle The propertyOracle used to access the relevant
	 *        configuration property.
	 * @return a Set of Strings, or null.
	 */
	static Set<String> getEnhancedTypes(PropertyOracle propertyOracle)
	{
		try
		{
			ConfigurationProperty prop = propertyOracle.getConfigurationProperty(WS_ENHANCED_CLASSES);
			return Collections.unmodifiableSet(new HashSet<String>(prop.getValues()));
		}
		catch (BadPropertyValueException e)
		{
			return null;
		}
	}

	public static String getStreamReadMethodNameFor(JType type)
	{
		return "read" + getCallSuffix(type);
	}

	public static String getStreamWriteMethodNameFor(JType type)
	{
		return "write" + getCallSuffix(type);
	}

	/**
	 * Returns <code>true</code> if warnings should not be emitted for final
	 * fields in serializable types.
	 */
	static boolean shouldSuppressNonStaticFinalFieldWarnings(TreeLogger logger, PropertyOracle propertyOracle)
	{
		return getBooleanProperty(logger, propertyOracle, WS_PROP_SUPPRESS_NON_STATIC_FINAL_FIELD_WARNINGS, false);
	}

	/**
	 * Computes a good name for a class related to the specified type, such that
	 * the computed name can be a top-level class in the same package as the
	 * specified type.
	 * <p>
	 * This method does not currently check for collisions between the
	 * synthesized name and an existing top-level type in the same package. It
	 * is actually tricky to do so, because on subsequent runs, we'll view our
	 * own generated classes as collisions. There's probably some trick we can
	 * use in the future to make it totally bulletproof.
	 * </p>
	 * 
	 * @param type the name of the base type, whose name will be built upon to
	 *        synthesize a new type name
	 * @param suffix a suffix to be used to make the new synthesized type name
	 * @return an array of length 2 such that the first element is the package
	 *         name and the second element is the synthesized class name
	 */
	public static String[] synthesizeTopLevelClassName(JClassType type, String suffix)
	{
		/*
		 * Gets the basic name of the type. If it's a nested type, the type name
		 * will contains dots.
		 */
		String className;
		String packageName;

		JType leafType = type.getLeafType();
		if (leafType.isPrimitive() != null)
		{
			className = leafType.getSimpleSourceName();
			packageName = "com.google.gwt.user.client.rpc.core";
		}
		else
		{
			JClassType classOrInterface = leafType.isClassOrInterface();
			assert (classOrInterface != null);
			className = classOrInterface.getName();
			packageName = classOrInterface.getPackage().getName();
		}

		JArrayType isArray = type.isArray();
		if (isArray != null)
		{
			className += "_Array_Rank_" + isArray.getRank();
		}

		/* Add the meaningful suffix. */
		className += suffix;

		/* Make it a top-level name. */
		className = className.replace('.', '_');

		return new String[] { packageName, className };
	}

	/**
	 * Determines whether a particular type needs to be cast to become its final
	 * type. Primitives and Strings do not, as they are read directly as the
	 * correct type. All other Objects need a cast, except for Object itself.
	 * 
	 * @param type the type in question
	 * @return <code>true</code> if the results of a read method must be cast,
	 *         otherwise <code>false</code>.
	 */
	static boolean typeNeedsCast(JType type)
	{
		return type.isPrimitive() == null && !type.getQualifiedSourceName().equals("java.lang.String")
				&& !type.getQualifiedSourceName().equals("java.lang.Object");
	}

	private static boolean getBooleanProperty(TreeLogger logger, PropertyOracle propertyOracle, String propertyName,
			boolean defaultValue)
	{
		try
		{
			SelectionProperty prop = propertyOracle.getSelectionProperty(logger, propertyName);
			String propVal = prop.getCurrentValue();
			if (propVal != null && propVal.length() > 0) { return Boolean.valueOf(propVal); }
		}
		catch (BadPropertyValueException e)
		{
			/* Just return the default value. */
		}
		return defaultValue;
	}

	/**
	 * Gets the suffix needed to make a call for a particular type. For example,
	 * the <code>int</code> class needs methods named "readInt" and "writeInt".
	 * 
	 * @param type the type in question
	 * @return the suffix of the method to call
	 */
	private static String getCallSuffix(JType type)
	{
		JParameterizedType isParameterized = type.isParameterized();
		if (isParameterized != null)
		{
			return getCallSuffix(isParameterized.getRawType());
		}
		else if (type.isPrimitive() != null)
		{
			if (type == JPrimitiveType.BOOLEAN)
			{
				return "Boolean";
			}
			else if (type == JPrimitiveType.BYTE)
			{
				return "Byte";
			}
			else if (type == JPrimitiveType.CHAR)
			{
				return "Char";
			}
			else if (type == JPrimitiveType.DOUBLE)
			{
				return "Double";
			}
			else if (type == JPrimitiveType.FLOAT)
			{
				return "Float";
			}
			else if (type == JPrimitiveType.INT)
			{
				return "Int";
			}
			else if (type == JPrimitiveType.LONG)
			{
				return "Long";
			}
			else if (type == JPrimitiveType.SHORT)
			{
				return "Short";
			}
			else
			{
				return null;
			}
		}
		else if (type.getQualifiedSourceName().equals("java.lang.String"))
		{
			return "String";
		}
		else
		{
			return "Object";
		}
	}
}
